{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Using Input Parameters to efficiently re-run simulations with different parameters\n",
    "\n",
    "The previous notebook described the PyBaMM pipeline and how you can use the `pybamm.Simulation` class to efficiently run simulations. Recall the pipeline was:\n",
    "\n",
    "1. Parameter replacement\n",
    "2. Discretisation\n",
    "3. Solver setup\n",
    "4. Solver solve\n",
    "5. Post-processing\n",
    "\n",
    "An obvious question is how can we efficiently re-run simulations with different parameters (step 1 in the pipeline)? Ideally we'd like to do this without having to re-build the model and discretisation each time, which as we've seen is a costly process."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Note: you may need to restart the kernel to use updated packages.\n"
     ]
    }
   ],
   "source": [
    "%pip install \"pybamm[plot,cite]\" -q    # install PyBaMM if it is not installed\n",
    "import numpy as np\n",
    "\n",
    "import pybamm"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "## Using Input Parameters\n",
    "\n",
    "The answer is to use input parameters and the [`pybamm.InputParameter`](https://docs.pybamm.org/en/stable/source/api/expression_tree/input_parameter.html) class. Input parameters are a special type of parameter that is *not* replaced by a value during step 1 of the pipeline, and can be used to replace many of the parameters in the model (as we'll see later this does not apply to some geometric parameters). \n",
    "\n",
    "If you use the `pybamm.ParameterVariables` class to set the parameters of your model, you can set any parameter in the model to be an input parameter by updating its parameter value to be the string `[input]`. For example, to set the current in the SPM model to be an input parameter, you can do the following:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = pybamm.lithium_ion.SPM()\n",
    "parameter_values = model.default_parameter_values\n",
    "parameter_values[\"Current function [A]\"] = \"[input]\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "If you are building up a model from scratch, you can also use `pybamm.InputParameter` directly to create an input parameter. For example, the code below creates an exponential decay model with a decay constant as an input parameter:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = pybamm.BaseModel()\n",
    "k = pybamm.InputParameter(\"k\")\n",
    "x = pybamm.Variable(\"x\")\n",
    "model.rhs = {x: -k * x}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Example\n",
    "\n",
    "Let's see how we can use input parameters to efficiently re-run simulations with different parameters. We'll start with a script that loops over the current to solve the model, and then show how we can use input parameters to do this more efficiently."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Average time taken: 0.193 seconds\n"
     ]
    }
   ],
   "source": [
    "import time\n",
    "\n",
    "average_time = 0\n",
    "n = 9\n",
    "model = pybamm.lithium_ion.SPM()\n",
    "solver = pybamm.IDAKLUSolver()\n",
    "params = model.default_parameter_values\n",
    "for current in np.linspace(-1.1, 1.0, n):\n",
    "    time_start = time.perf_counter()\n",
    "    params[\"Current function [A]\"] = current\n",
    "    sim = pybamm.Simulation(model, solver=solver, parameter_values=params)\n",
    "    sol = sim.solve([0, 3600])\n",
    "    t_evals = np.linspace(0, 3600, 100)\n",
    "    voltage = sol[\"Terminal voltage [V]\"](t_evals)\n",
    "    time_end = time.perf_counter()\n",
    "    average_time += time_end - time_start\n",
    "print(f\"Average time taken: {average_time / n:.3f} seconds\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The most important thing to note about this script is that we are creating the simulation object on every iteration of this loop. This means we are running though all the steps of the pipeline, rebuilding the model and performing the discretisations, at each iteration of the loop. We do this even though most of the structure of the model, and in particular the numerical discretisations of the spatial gradients, is unchanged. As we've seen previously, the discretisation step is often the most expensive part of the pipeline, so we'd like to avoid repeating it if possible.\n",
    "\n",
    "Now let's see how we will use input parameters to do this more efficiently. To do this we will move the simulation object creation outside of the loop, and use an input parameter for the current instead."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Average time taken: 0.033 seconds\n"
     ]
    }
   ],
   "source": [
    "average_time = 0\n",
    "n = 10\n",
    "model = pybamm.lithium_ion.SPM()\n",
    "solver = pybamm.IDAKLUSolver()\n",
    "params = model.default_parameter_values\n",
    "params[\"Current function [A]\"] = \"[input]\"\n",
    "sim = pybamm.Simulation(model, solver=solver, parameter_values=params)\n",
    "for current in np.linspace(0.1, 1.0, n):\n",
    "    time_start = time.perf_counter()\n",
    "    sol = sim.solve([0, 3600], inputs={\"Current function [A]\": current})\n",
    "    t_evals = np.linspace(0, 3600, 100)\n",
    "    voltage = sol[\"Terminal voltage [V]\"](t_evals)\n",
    "    time_end = time.perf_counter()\n",
    "    average_time += time_end - time_start\n",
    "print(f\"Average time taken: {average_time / n:.3f} seconds\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can see that we've improved the performance of our loop significantly by moving the creation of the simulation object outside of the loop, thus avoiding the need to rebuild the model from scratch at each iteration. \n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Limitations"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "### Geometric parameters cannot be input parameters\n",
    "\n",
    "Input parameters cannot be used for parameters that are used during the descretisation step. These are generally parameters affecting the geometry, such as the electrode or separator thicknesses. If you try to use an input parameter for a parameter that affects the discretisation, you will get an error, most likely during the discretisation step. For example, these are the parameters that fail for the SPM model, along with the error messages:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Failed for parameter Separator thickness [m]. Error was Cannot interpret 'Addition(-0x1e5235efb4a04db1, +, children=['0.0001', 'Separator thickness [m]'], domains={})' as a data type\n",
      "Traceback (most recent call last):\n",
      "  File \"/tmp/ipykernel_1968898/3514874421.py\", line 12, in <module>\n",
      "    sim.solve([0, 3600], inputs={param: original_param})\n",
      "  File \"/home/mrobins/git/PyBaMM/src/pybamm/simulation.py\", line 472, in solve\n",
      "    self.build(initial_soc=initial_soc, inputs=inputs)\n",
      "  File \"/home/mrobins/git/PyBaMM/src/pybamm/simulation.py\", line 328, in build\n",
      "    self._mesh = pybamm.Mesh(self._geometry, self._submesh_types, self._var_pts)\n",
      "  File \"/home/mrobins/git/PyBaMM/src/pybamm/meshes/meshes.py\", line 117, in __init__\n",
      "    self[domain] = submesh_types[domain](geometry[domain], submesh_pts[domain])\n",
      "  File \"/home/mrobins/git/PyBaMM/src/pybamm/meshes/meshes.py\", line 301, in __call__\n",
      "    return self.submesh_type(lims, npts, **self.submesh_params)\n",
      "  File \"/home/mrobins/git/PyBaMM/src/pybamm/meshes/one_dimensional_submeshes.py\", line 130, in __init__\n",
      "    edges = np.linspace(spatial_lims[\"min\"], spatial_lims[\"max\"], npts + 1)\n",
      "  File \"/home/mrobins/git/PyBaMM/env/lib/python3.10/site-packages/numpy/core/function_base.py\", line 132, in linspace\n",
      "    dt = result_type(start, stop, float(num))\n",
      "TypeError: Cannot interpret 'Addition(-0x1e5235efb4a04db1, +, children=['0.0001', 'Separator thickness [m]'], domains={})' as a data type\n",
      "\n",
      "Failed for parameter Negative electrode thickness [m]. Error was Cannot interpret 'InputParameter(0x6c630c7d1f75ef82, Negative electrode thickness [m], children=[], domains={})' as a data type\n",
      "Traceback (most recent call last):\n",
      "  File \"/tmp/ipykernel_1968898/3514874421.py\", line 12, in <module>\n",
      "    sim.solve([0, 3600], inputs={param: original_param})\n",
      "  File \"/home/mrobins/git/PyBaMM/src/pybamm/simulation.py\", line 472, in solve\n",
      "    self.build(initial_soc=initial_soc, inputs=inputs)\n",
      "  File \"/home/mrobins/git/PyBaMM/src/pybamm/simulation.py\", line 328, in build\n",
      "    self._mesh = pybamm.Mesh(self._geometry, self._submesh_types, self._var_pts)\n",
      "  File \"/home/mrobins/git/PyBaMM/src/pybamm/meshes/meshes.py\", line 117, in __init__\n",
      "    self[domain] = submesh_types[domain](geometry[domain], submesh_pts[domain])\n",
      "  File \"/home/mrobins/git/PyBaMM/src/pybamm/meshes/meshes.py\", line 301, in __call__\n",
      "    return self.submesh_type(lims, npts, **self.submesh_params)\n",
      "  File \"/home/mrobins/git/PyBaMM/src/pybamm/meshes/one_dimensional_submeshes.py\", line 130, in __init__\n",
      "    edges = np.linspace(spatial_lims[\"min\"], spatial_lims[\"max\"], npts + 1)\n",
      "  File \"/home/mrobins/git/PyBaMM/env/lib/python3.10/site-packages/numpy/core/function_base.py\", line 132, in linspace\n",
      "    dt = result_type(start, stop, float(num))\n",
      "TypeError: Cannot interpret 'InputParameter(0x6c630c7d1f75ef82, Negative electrode thickness [m], children=[], domains={})' as a data type\n",
      "\n",
      "Failed for parameter Positive electrode thickness [m]. Error was Cannot interpret 'Addition(-0x30e83b9fe1971dbd, +, children=['0.000125', 'Positive electrode thickness [m]'], domains={})' as a data type\n",
      "Traceback (most recent call last):\n",
      "  File \"/tmp/ipykernel_1968898/3514874421.py\", line 12, in <module>\n",
      "    sim.solve([0, 3600], inputs={param: original_param})\n",
      "  File \"/home/mrobins/git/PyBaMM/src/pybamm/simulation.py\", line 472, in solve\n",
      "    self.build(initial_soc=initial_soc, inputs=inputs)\n",
      "  File \"/home/mrobins/git/PyBaMM/src/pybamm/simulation.py\", line 328, in build\n",
      "    self._mesh = pybamm.Mesh(self._geometry, self._submesh_types, self._var_pts)\n",
      "  File \"/home/mrobins/git/PyBaMM/src/pybamm/meshes/meshes.py\", line 117, in __init__\n",
      "    self[domain] = submesh_types[domain](geometry[domain], submesh_pts[domain])\n",
      "  File \"/home/mrobins/git/PyBaMM/src/pybamm/meshes/meshes.py\", line 301, in __call__\n",
      "    return self.submesh_type(lims, npts, **self.submesh_params)\n",
      "  File \"/home/mrobins/git/PyBaMM/src/pybamm/meshes/one_dimensional_submeshes.py\", line 130, in __init__\n",
      "    edges = np.linspace(spatial_lims[\"min\"], spatial_lims[\"max\"], npts + 1)\n",
      "  File \"/home/mrobins/git/PyBaMM/env/lib/python3.10/site-packages/numpy/core/function_base.py\", line 132, in linspace\n",
      "    dt = result_type(start, stop, float(num))\n",
      "TypeError: Cannot interpret 'Addition(-0x30e83b9fe1971dbd, +, children=['0.000125', 'Positive electrode thickness [m]'], domains={})' as a data type\n",
      "\n"
     ]
    }
   ],
   "source": [
    "import traceback\n",
    "\n",
    "model = pybamm.lithium_ion.SPM()\n",
    "model_params = model.get_parameter_info()\n",
    "for param in model_params:\n",
    "    if model_params[param][1] == \"Parameter\":\n",
    "        params = model.default_parameter_values\n",
    "        original_param = params[param]\n",
    "        params[param] = \"[input]\"\n",
    "        sim = pybamm.Simulation(model, parameter_values=params)\n",
    "        try:\n",
    "            sim.solve([0, 3600], inputs={param: original_param})\n",
    "        except Exception as e:\n",
    "            print(f\"Failed for parameter {param}. Error was {e}\")\n",
    "            tb = traceback.format_exc()\n",
    "            print(tb)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "env",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
